PrivateLinkを通じてSSMを使用するCFnテンプレートを作ってみた
なつのうたを聴きながら、暑い暑いと連日思っています。コウペンちゃんが可愛いです。
日本の夏はとっても暑いので、プライベートサブネット配下のインスタンスをPrivateLinkを通じてSSMを使用して操作したくなりました。
ついでにCloudFormationのテンプレートも作成したくなったのでつくりました。
VPC Endpointとは
本題に入る前に、VPC Endpointについて少しだけ記載します。
2015年の4月まで、EC2インスタンスから各種AWSリソース(S3、DynamoDB、CodeCommit...)に対してアクセスするためにはパブリックIPアドレスが必要でした。
パブリックサブネット配下のインスタンスは、インスタンス -> Internet GW -> S3
のような経路で、プライベートサブネット配下のインスタンスは、インスタンス -> NAT GW -> S3
のような経路でアクセスする必要がありました。
NAT GWが必要になったり、インターネットを通じてアクセスするため経路をAWS内に閉じ込めるということができませんね。
同年の5月にVPC Endpointが新機能として提供され、S3に対してインターネットを通さずにアクセスできるようになりました。
インスタンスからS3への通信をエンドポイントを通じて行います。
ルートテーブル単位でエンドポイントへのルーティングを行います。
なので、通信経路はインスタンス -> エンドポイント -> S3
となります。
このルートテーブルを使用して制御するエンドポイントのことを、ゲートウェイVPCエンドポイントと呼び、S3とDynamoDBのみ対応しています。
2017年11月にインターフェイスVPCエンドポイントという方式のエンドポイントが発表されました。
この方式では、VPC配下にENIを作成して、ENIと各種サービスのエンドポイントをPrivateLinkで繋ぎます。
ENIを使用するのでセキュリティグループを紐づける必要があり、またサブネット単位で制御することになります。
なので、インスタンス -> ENI -> SSM Endpoint
のような経路で通信を行います。
実装について
冒頭でも記載した通り、CloudFormationのテンプレートを作成していきます。
実際に使用する場合は色々拡張してください。
Resources
配下で、下記のパラメータを使用しているので頭の部分だけ載せておきます。
AWSTemplateFormatVersion: 2010-09-09 Description: SSM for Private Subnet Parameters: Prefix: Description: Prefix Type: String Default: thin-ssm AZ1: Description: AZ1 Type: String Default: a AMZN2: Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
今後使う予定のAZとAMI IDのみ記載しています。
SSMのエンドポイントはap-northeast-1dを現在サポートしていないのでご注意ください。
VPC
まずは、大枠のVPCから書いていきましょう。CIDRやタグなどはご自由にどうぞ。
ハイライトがある5、6行目のEnableDnsSupport
とEnableDnsHostnames
は必ず有効化してください。
特に、EnableDnsHostnames
のデフォルトがfalseなので、書き忘れていて動かない...とかよくありそうです。
VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 192.168.0.0/24 EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub "${Prefix}-vpc" PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${Prefix}-private-rtb" PrivateSubnet: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Sub "${AWS::Region}${AZ1}" VpcId: !Ref VPC CidrBlock: 192.168.0.0/28 Tags: - Key: Name Value: !Sub "${Prefix}-private-subnet" PrivateSubnetAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable
Security Group
1つ目のSecurity Group(SG)はEC2インスタンスに紐づけるためのものです。
特に外部と通信するとかの要件がなかったので、何もルールをつけていません。
2つ目のVPCEndpoint用のSecurity Group(SSMEndpointSG)では、VPC内のインスタンスからHTTPSでの通信を許可しています。
このルールは必ず必要なので作成しましょう。
SG: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref VPC GroupName: !Sub "${Prefix}-sg" GroupDescription: !Sub "${Prefix}-sg" Tags: - Key: Name Value: !Sub "${Prefix}-sg" SSMEndpointSG: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref VPC GroupName: !Sub "${Prefix}-ssm-endpoint-sg" GroupDescription: !Sub "${Prefix}-ssm-endpoint-sg" Tags: - Key: Name Value: !Sub "${Prefix}-ssm-endpoint-sg" SSMEndpointSGIngress1: Type: "AWS::EC2::SecurityGroupIngress" Properties: GroupId: !Ref SSMEndpointSG IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 192.168.0.0/24
VPC Endpoint
VPC Endpointを作っていきましょう。
SSMのドキュメントに書いてある通りに下記のエンドポイントを作成していきます。また今回は東京リージョンに作成するので、region
の値はap-northeast-1
になります。
ssmmessagesのみ作成するかは任意選択となっています。
セッションマネージャーを使用したい場合にssmmessagesのエンドポイントを作成します。
- com.amazonaws.
region
.ssm - com.amazonaws.
region
.ec2messages - com.amazonaws.
region
.ec2: - com.amazonaws.
region
.ssmmessages - com.amazonaws.
region
.s3
インタフェース型のVPC Endpointは、PrivateDnsEnabled
をtrueにしてください。
注意点はこのくらいです。後は今までの流れを組んでよしなに書いていきます。
SSMEndpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG EC2MessageEndpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG EC2Endpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG SSMAgentEndpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG S3Endpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3" VpcId: !Ref VPC RouteTableIds: - !Ref PrivateRouteTable
EC2 Instance
インスタンスプロファイルを指定します(IAMの項で作成します)。
それ以外についてはよしなに書いていきます。
OSが要件を満たしているかどうかは、こちらを確認してください。
Instance: Type: "AWS::EC2::Instance" Properties: IamInstanceProfile: !Ref ServerProfile ImageId: !Ref AMZN2 InstanceType: t3.small BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeType: gp2 VolumeSize: 8 DeleteOnTermination: true NetworkInterfaces: - AssociatePublicIpAddress: false DeviceIndex: "0" DeleteOnTermination: true GroupSet: - !Ref SG SubnetId: !Ref PrivateSubnet Tags: - Key: Name Value: !Sub "${Prefix}-Instance"
IAM
SSM用のポリシー作成の方針として、AmazonSSMManagedInstanceCore
をベースにして必要なポリシーを追加していく必要があります。
今回は最小限の設定で問題ないので、こちらを参考にして最小のS3アクセスのみを追加しています。
作成したインスタンスプロファイルをEC2側で参照するようにしています。
ServerRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" S3BucketPolicyForSSM: Type: AWS::IAM::Policy Properties: PolicyName: S3BucketPolicyForSSM Roles: - !Ref ServerRole PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "s3:GetObject" Resource: - !Sub "arn:aws:s3:::aws-ssm-${AWS::Region}/*" - !Sub "arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*" - !Sub "arn:aws:s3:::amazon-ssm-${AWS::Region}/*" - !Sub "arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*" - !Sub "arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*" - !Sub "arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*" ServerProfile: Type: "AWS::IAM::InstanceProfile" Properties: Path: "/" Roles: - Ref: ServerRole InstanceProfileName: !Sub "${Prefix}-Server"
全体像
今までの内容をまとめるとこのようなテンプレートが出来上がりました。
これで、プライベートサブネット配下のEC2インスタンスに対してSSMで作業ができますね。
AWSTemplateFormatVersion: 2010-09-09 Description: SSM for Private Subnet Parameters: Prefix: Description: Prefix Type: String Default: thin-ssm AZ1: Description: AZ1 Type: String Default: a AMZN2: Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2" Resources: #----------------------------------------------------------------------------- # VPC #----------------------------------------------------------------------------- VPC: Type: AWS::EC2::VPC Properties: CidrBlock: 192.168.0.0/24 EnableDnsSupport: true EnableDnsHostnames: true Tags: - Key: Name Value: !Sub "${Prefix}-vpc" PrivateRouteTable: Type: AWS::EC2::RouteTable Properties: VpcId: !Ref VPC Tags: - Key: Name Value: !Sub "${Prefix}-private-rtb" PrivateSubnet: Type: AWS::EC2::Subnet Properties: AvailabilityZone: !Sub "${AWS::Region}${AZ1}" VpcId: !Ref VPC CidrBlock: 192.168.0.0/28 Tags: - Key: Name Value: !Sub "${Prefix}-private-subnet" PrivateSubnetAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: SubnetId: !Ref PrivateSubnet RouteTableId: !Ref PrivateRouteTable #----------------------------------------------------------------------------- # Securty Group #----------------------------------------------------------------------------- SG: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref VPC GroupName: !Sub "${Prefix}-sg" GroupDescription: !Sub "${Prefix}-sg" Tags: - Key: Name Value: !Sub "${Prefix}-sg" SSMEndpointSG: Type: "AWS::EC2::SecurityGroup" Properties: VpcId: !Ref VPC GroupName: !Sub "${Prefix}-ssm-endpoint-sg" GroupDescription: !Sub "${Prefix}-ssm-endpoint-sg" Tags: - Key: Name Value: !Sub "${Prefix}-ssm-endpoint-sg" #----------------------------------------------------------------------------- # SG(Security Group) Rules #----------------------------------------------------------------------------- SSMEndpointSGIngress1: Type: "AWS::EC2::SecurityGroupIngress" Properties: GroupId: !Ref SSMEndpointSG IpProtocol: tcp FromPort: 443 ToPort: 443 CidrIp: 192.168.0.0/24 #----------------------------------------------------------------------------- # Endpoint #----------------------------------------------------------------------------- SSMEndpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG EC2MessageEndpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG EC2Endpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG SSMAgentEndpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages" VpcEndpointType: Interface PrivateDnsEnabled: true VpcId: !Ref VPC SubnetIds: - !Ref PrivateSubnet SecurityGroupIds: - !Ref SSMEndpointSG S3Endpoint: Type: AWS::EC2::VPCEndpoint Properties: ServiceName: !Sub "com.amazonaws.${AWS::Region}.s3" VpcId: !Ref VPC RouteTableIds: - !Ref PrivateRouteTable #----------------------------------------------------------------------------- # EC2 Instance #----------------------------------------------------------------------------- Instance: Type: "AWS::EC2::Instance" Properties: IamInstanceProfile: !Ref ServerProfile ImageId: !Ref AMZN2 InstanceType: t3.small BlockDeviceMappings: - DeviceName: /dev/xvda Ebs: VolumeType: gp2 VolumeSize: 8 DeleteOnTermination: true NetworkInterfaces: - AssociatePublicIpAddress: false DeviceIndex: "0" DeleteOnTermination: true GroupSet: - !Ref SG SubnetId: !Ref PrivateSubnet Tags: - Key: Name Value: !Sub "${Prefix}-Instance" #----------------------------------------------------------------------------- # IAM #----------------------------------------------------------------------------- ServerRole: Type: "AWS::IAM::Role" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "ec2.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" S3BucketPolicyForSSM: Type: AWS::IAM::Policy Properties: PolicyName: S3BucketPolicyForSSM Roles: - !Ref ServerRole PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: "s3:GetObject" Resource: - !Sub "arn:aws:s3:::aws-ssm-${AWS::Region}/*" - !Sub "arn:aws:s3:::aws-windows-downloads-${AWS::Region}/*" - !Sub "arn:aws:s3:::amazon-ssm-${AWS::Region}/*" - !Sub "arn:aws:s3:::amazon-ssm-packages-${AWS::Region}/*" - !Sub "arn:aws:s3:::${AWS::Region}-birdwatcher-prod/*" - !Sub "arn:aws:s3:::patch-baseline-snapshot-${AWS::Region}/*" ServerProfile: Type: "AWS::IAM::InstanceProfile" Properties: Path: "/" Roles: - Ref: ServerRole InstanceProfileName: !Sub "${Prefix}-Server"
このテンプレートを元にスタックを作成するには下記のコマンドを実行します。
IAMを操作するのでcapabilitiesを追加しています。
aws cloudformation create-stack \ --stack-name ssm-thin-sampple \ --template-url https://gist.githubusercontent.com/37108/ad4519a138addf291000e44491ec46ed/raw/e9bcf573db6c18b3d756f8c73dba781d799acd6b/thin-private-subnet-ssm.yaml \ --capabilities CAPABILITY_NAMED_IAM \ --region ap-northeast-1
さいごに
VPC Endpointは便利ですね。自分のやろうとしたことに似たテンプレートが見つからなかったので自分で作って公開してみました。
ぜひ、なつのうたを聴いて癒されてください。